<?php
// Copyright 2011 Zeptonic Ek. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
//    conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
//    of conditions and the following disclaimer in the documentation and/or other materials
//    provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY Zeptonic Ek ``AS IS'' AND ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Zeptonic Ek OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// 
// The views and conclusions contained in the software and documentation are those of the 
// authors and should not be interpreted as representing official policies, 
// either expressed or implied, of Zeptonic Ek.

/**
 * Css parsare.
 * 
 * @category Golfscraper
 * @package  Golfscraper
 * @author   Anders Axelsson <anders@zeptonic.com>
 * @author   Mattias Tiger <mattias@zeptonic.com>
 * @link     http://www.zeptonic.com
 */
class Css_parser {
	/**
	 * @var String Lista med alla noder av tag-typ
	 */
	private $tag;
	
	/**
	 * @var String Lista med alla noder av class-typ
	 */
	private $class;
	
	/**
	 * @var String Lista med alla noder av id-typ
	 */
	private $id;
	
	/**
	 * Startpunkt för css parsning.
	 *
	 * @return Void
	 */
	public function __construct() {
		$this->tag = array();
		$this->class = array();
		$this->id = array();
	}
	
	/**
	 * Hanterar parsning av css-filers innehåll.
	 * Alla kommentarer tas bort och css-kommandona sorteras
	 * efter tag, class respektive id.
	 * 
	 * @param String $css En css-fils innehåll
	 * 
	 * @return Void
	 */
	public function load($css) {
		// tar bort /*...*/-kommentarer
		$reg = '/(\/\*).*?(\*\/)/ims';
		$css = preg_split($reg, $css);
		$css = implode($css, "");
		
		// tar bort //-kommentarer
		$reg = '/(\/\/)[^\n|^\;]*(\n)/ims';
		$css = preg_split($reg, $css);
		$css = implode($css, "");
		
		// finner alla data-conteinrar, som omslutas av { respektive }, samt säntlade data-contenrar
		$reg = "/\{(([^{}]*|(?R))*)\}/ims";
		preg_match_all($reg, $css, $out, PREG_PATTERN_ORDER);
		$content = preg_split($reg, $css);
		
		// för varje data-container tas namn, typ och data ut och konverteras till css-noder
		foreach($out[1] as $key => $match) {
			$content[$key] = trim($content[$key]);
			if($content[$key][0] == ".") {
				// class
				$this->add_class(substr($content[$key], 1, strlen($content[$key])), $match);
			} elseif($content[$key][0] == "#") {
				// id
				$this->add_id(substr($content[$key], 1, strlen($content[$key])), $match);
			} elseif($content[$key][0] == "@") {
				// ignorerar noder av typ @, tex @media
			} else {
				// tagg
				$this->add_tag($content[$key], $match);
			}
		}
	}
	
	/**
	 * Sammanfogar alla noder till en sträng.
	 *
	 * @return String
	 */
	public function save() {
		$css = "";
		$css .= $this->css_types_to_text($this->tag, "");
		$css .= $this->css_types_to_text($this->class, ".");
		$css .= $this->css_types_to_text($this->id, "#");
		return $css;
	}
	
	/**
	 * Lägger till en css-nod av typ class.
	 * 
	 * @param String $class Class-namn
	 * @param String $data Data i nodens data-container
	 *
	 * @return Void
	 */
	public function add_class($class, $data) {
		if(!isset($this->class[$class]))
			$this->class[$class] = new css_node("class", $class);
		$this->class[$class]->load($data);
	}
	
	/**
	 * Lägger till en css-nod av typ id.
	 * 
	 * @param String $id Id-namn
	 * @param String $data Data i nodens data-container
	 *
	 * @return Void
	 */
	public function add_id($id, $data) {
		if(!isset($this->id[$id]))
			$this->id[$id] = new css_node("id", $id);
		$this->id[$id]->load($data);
	}
	
	/**
	 * Lägger till en css-nod av typ tag.
	 * 
	 * @param String $tag Tag-namn
	 * @param String $data Data i nodens data-container
	 *
	 * @return Void
	 */
	public function add_tag($tag, $data) {
		if(!isset($this->tag[$tag]))
			$this->tag[$tag] = new css_node("tag", $tag);
		$this->tag[$tag]->load($data);
	}
	
	/**
	 * Lägger till ett attribut till en css-nod.
	 * Om noden inte finns i listan skapas den.
	 * 
	 * @param String $node_id Id för noden (.class_namn, #id_namn, eller tag_namn)
	 * @param String $attribute_name Namn på attributet
	 * @param String $attribute_value Värde till attributet
	 *
	 * @return Void
	 */
	public function set($node_id, $attribute_name, $attribute_value) {
		$node_type = $node_id[0];
		$node_name = substr($node_id, 1);
		if($node_type == ".") {
			// class
			if(!isset($this->class[$node_name])) {
				$this->class[$node_name] = new css_node("class", $node_name);
			}
			$this->class[$node_name]->set_attribute($attribute_name, $attribute_value);
		} elseif($node_type == "#") {
			// id
			if(!isset($this->id[$node_name])) {
				$this->id[$node_name] = new css_node("id", $node_name);
			}
			$this->id[$node_name]->set_attribute($attribute_name, $attribute_value);
		} else {
			// tag
			if(!isset($this->tag[$node_id])) {
				$this->tag[$node_id] = new css_node("tag", $node_id);
			}
			$this->tag[$node_id]->set_attribute($attribute_name, $attribute_value);
		}
	}
	
	/**
	 * Ta bort en css-nod.
	 *
	 * @param String $node_id Id för noden (.class_namn, #id_namn, eller tag_namn)
	 *
	 * @return Void
	 */
	public function remove($node_id) {
		$node_type = $node_id[0];
		$node_name = substr($node_id, 1);
		if(node_type == ".") {
			//class
			unset($this->class[node_name]);
		} elseif(node_type == "#") {
			//id
			unset($this->id[node_name]);
		} else {
			//tag
			unset($this->tag[$node_id]);
		}
	}
	
	/**
	 * Tar bort alla css-noder av samma typ som ges av prefixet till nod-id:t.
	 *
	 * @param String $node_id Id för noden (.class_namn, #id_namn, eller tag_namn) eller bara prefixet
	 *
	 * @return Void
	 */
	public function remove_all($node_id) {
		$node_type = $node_id[0];
		$node_name = substr($node_id, 1);
		if($node_type == ".") {
			//class
			$this->remove_all_items_with_prefix_in_array($node_id, $this->class);
		} elseif($node_type == "#") {
			//id
			$this->remove_all_items_with_prefix_in_array($node_id, $this->id);
		} else {
			//tag
			$this->remove_all_items_with_prefix_in_array($node_id, $this->tag);
		}
	}
	
	/**
	 * Ta bort ett attribut.
	 *
	 * @param String $node_id Id för noden (.class_namn, #id_namn, eller tag_namn) eller bara prefixet
	 * @param String $attribut_name Namn på attributet
	 *
	 * @return Void
	 */
	public function remove_attribute($node_id, $attribute_name) {
		$node_type = $node_id[0];
		$node_name = substr($node_id, 1);
		if($node_type == ".") {
			if(isset($this->class[$node_name])) {
				$node = $this->class[$node_name];
				$node->unset_attribute($attribute_name);
			}
		} elseif($node_type == "#") {
			if(isset($this->id[$node_name])) {
				$node = $this->id[$node_name];
				$node->unset_attribute($attribute_name);
			}
		} else {
			if(isset($this->tag[$node_id])) {
				$node = $this->tag[$node_id];
				$node->unset_attribute($attribute_name);
			}
		}
	}
	
	/**
	 * Sätter alla attribut i alla noder med ett speciellt namn och har numeriskt värde 
	 * till ett maxvärde om dess nuvarande värde överstiger maxvärdet. 
	 *
	 * @param String $attribute_name Attributnamn
	 * @param String $limit Maxvärde
	 *
	 * @return Void
	 */
	public function set_all_attribute_less_than($attribute_name, $limit) {
		foreach($this->class as $class) {
			$this->set_attribute_less_than($class, $attribute_name, $limit);
		}
		
		foreach($this->id as $id) {
			$this->set_attribute_less_than($id, $attribute_name, $limit);
		}
		
		foreach($this->tag as $tag) {
			$this->set_attribute_less_than($tag, $attribute_name, $limit);
		}
	}
	
	/**
	 * Sätter, i en speciell nod, ett attribut med ett speciellt namn och har numeriskt värde 
	 * till ett maxvärde om dess nuvarande värde överstiger maxvärdet. 
	 *
	 * @param Css_node $node Nod
	 * @param String $attribute_name Attributnamn
	 * @param String $limit Maxvärde
	 *
	 * @return Void
	 */
	private function set_attribute_less_than($node, $attribute_name, $limit) {
		if($node->has_attribute($attribute_name)) {
			if((int)$node->get_attribute($attribute_name) >= (int)$limit) {
				$node->set_attribute($attribute_name, $limit);
			}
		}
	}
	
	/**
	 * Tar bort alla noder med ett speciellt prefix 
	 * (. för class, # för id och allt annat för tag (inget prefix)).
	 *
	 * @param String $prefix Prefix
	 * @param Array $node_list Nodlista att genomsöka
	 *
	 * @return Void
	 */
	private function remove_all_items_with_prefix_in_array($prefix, $node_list) {
		foreach($node_list as $name => $content) {
			$pos = stripos($node_list, $prefix);
			if($pos == 0) {
				unset($node_list[$name]);
			}
		}
	}
	
	/**
	 * Konverterar en nodlista till en sträng så som en css-fil ser ut.
	 *
	 * @param Array $node_list Nodlista att konvertera
	 * @param String $prefix Prefix som nodernas namn ska ha
	 *
	 * @return String
	 */
	private function css_types_to_text($node_list, $prefix) {
		$css = "";
		foreach($node_list as $css_node) {
			$css .= $prefix . $css_node->get_name() . " " . $this->css_context($css_node);
		}
		return $css;
	}
	
	/**
	 * Skapar data-containern som en sträng för nodens attribut 
	 * och lägger in varje attribut med korrekt format.
	 *
	 * @param $css_node Nod med attribut
	 *
	 * @return String
	 */
	private function css_context($css_node) {
		$c = "{\n";
		$node_attributes = $css_node->get_attributes();
		foreach($node_attributes as $name => $value) {
			$c .= " " . $name . ": " . $value . ";\n";
		}
		$c .= "}\n\n";
		return $c;
	}
}
?>